home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2005 October / PCWOCT05.iso / Software / FromTheMag / XAMPP 1.4.14 / xampp-win32-1.4.14-installer.exe / xampp / php / pear / File / CSV.php next >
PHP Script  |  2004-03-24  |  15KB  |  416 lines

  1. <?php
  2. // +----------------------------------------------------------------------+
  3. // | PHP Version 4                                                        |
  4. // +----------------------------------------------------------------------+
  5. // | Copyright (c) 2002-2003 Tomas Von Veschler Cox                            |
  6. // +----------------------------------------------------------------------+
  7. // | This source file is subject to version 2.0 of the PHP license,       |
  8. // | that is bundled with this package in the file LICENSE, and is        |
  9. // | available at through the world-wide-web at                           |
  10. // | http://www.php.net/license/2_02.txt.                                 |
  11. // | If you did not receive a copy of the PHP license and are unable to   |
  12. // | obtain it through the world-wide-web, please send a note to          |
  13. // | license@php.net so we can mail you a copy immediately.               |
  14. // +----------------------------------------------------------------------+
  15. // | Authors: Tomas V.V.Cox <cox@idecnet.com>                             |
  16. // |                                                                      |
  17. // +----------------------------------------------------------------------+
  18. //
  19. // $Id: CSV.php,v 1.13 2003/01/04 11:54:55 mj Exp $
  20.  
  21. require_once 'PEAR.php';
  22. require_once 'File.php';
  23.  
  24. /**
  25. * File class for handling CSV files (Comma Separated Values), a common format
  26. * for exchanging data.
  27. *
  28. * TODO:
  29. *  - Usage example and Doc
  30. *  - Use getPointer() in discoverFormat
  31. *  - Add a line counter for being able to output better error reports
  32. *  - Store the last error in GLOBALS and add File_CSV::getLastError()
  33. *
  34. * Wish:
  35. *  - Support Mac EOL format
  36. *  - Other methods like readAll(), writeAll(), numFields(), numRows()
  37. *  - Try to detect if a CSV has header or not in discoverFormat()
  38. *
  39. * Known Bugs:
  40. * (they has been analyzed but for the moment the impact in the speed for
  41. *  properly handle this uncommon cases is too high and won't be supported)
  42. *  - A field which is composed only by a single quoted separator (ie -> ;";";)
  43. *    is not handled properly
  44. *  - When there is exactly one field minus than the expected number and there
  45. *    is a field with a separator inside, the parser will throw the "wrong count" error
  46. *
  47. * @author Tomas V.V.Cox <cox@idecnet.com>
  48. * @package File
  49. */
  50. class File_CSV
  51. {
  52.     /**
  53.     * This raiseError method works in a different way. It will always return
  54.     * false (an error occurred) but it will call PEAR::raiseError() before
  55.     * it. If no default PEAR global handler is set, will trigger an error.
  56.     *
  57.     * @param string $error The error message
  58.     * @return bool always false
  59.     */
  60.     function raiseError($error)
  61.     {
  62.         // If a default PEAR Error handler is not set trigger the error
  63.         // XXX Add a PEAR::isSetHandler() method?
  64.         if ($GLOBALS['_PEAR_default_error_mode'] == PEAR_ERROR_RETURN) {
  65.             PEAR::raiseError($error, null, PEAR_ERROR_TRIGGER, E_USER_WARNING);
  66.         } else {
  67.             PEAR::raiseError($error);
  68.         }
  69.         return false;
  70.     }
  71.  
  72.     /**
  73.     * Checks the configuration given by the user
  74.     *
  75.     * @param array  &$conf  The configuration assoc array
  76.     * @param string &$error The error will be written here if any
  77.     */
  78.     function _conf(&$conf, &$error)
  79.     {
  80.         // check conf
  81.         if (!is_array($conf)) {
  82.             return $error = "Invalid configuration";
  83.         }
  84.         if (isset($conf['sep'])) {
  85.             if (strlen($conf['sep']) != 1) {
  86.                 return $error = 'Separator can only be one char';
  87.             }
  88.         } else {
  89.             return $error = 'Missing separator (the "sep" key)';
  90.         }
  91.         if (!isset($conf['fields']) || !is_numeric($conf['fields'])) {
  92.             return $error = 'The number of fields must be numeric (the "fields" key)';
  93.         }
  94.         if (isset($conf['quote'])) {
  95.             if (strlen($conf['quote']) != 1) {
  96.                 return $error = 'The quote char must be one char (the "quote" key)';
  97.             }
  98.         } else {
  99.             $conf['quote'] = null;
  100.         }
  101.         if (!isset($conf['crlf'])) {
  102.             $conf['crlf'] = "\n";
  103.         }
  104.     }
  105.  
  106.     /**
  107.     * Return or create the file descriptor associated with a file
  108.     *
  109.     * @param string $file The name of the file
  110.     * @param array  &$conf The configuration
  111.     * @param string $mode The open node (ex: FILE_MODE_READ or FILE_MODE_WRITE)
  112.     *
  113.     * @return mixed A file resource or false
  114.     */
  115.     function getPointer($file, &$conf, $mode = FILE_MODE_READ)
  116.     {
  117.         static $resources  = array();
  118.         static $config;
  119.         if (isset($resources[$file])) {
  120.             $conf = $config;
  121.             return $resources[$file];
  122.         }
  123.         File_CSV::_conf($conf, $error);
  124.         if ($error) {
  125.             return File_CSV::raiseError($error);
  126.         }
  127.         $config = $conf;
  128.         PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
  129.         $fp = &File::_getFilePointer($file, $mode);
  130.         PEAR::popErrorHandling();
  131.         if (PEAR::isError($fp)) {
  132.             return File_CSV::raiseError($fp);
  133.         }
  134.         $resources[$file] = $fp;
  135.  
  136.         if ($mode == FILE_MODE_READ && !empty($conf['header'])) {
  137.             if (!File_CSV::read($file, $conf)) {
  138.                 return false;
  139.             }
  140.         }
  141.         return $fp;
  142.     }
  143.  
  144.     /**
  145.     * Unquote data
  146.     *
  147.     * @param string $field The data to unquote
  148.     * @param string $quote The quote char
  149.     * @return string the unquoted data
  150.     */
  151.     function unquote($field, $quote)
  152.     {
  153.         // Incase null fields (form: ;;)
  154.         if (!strlen($field)) {
  155.             return $field;
  156.         }
  157.         if ($quote && $field{0} == $quote && $field{strlen($field)-1} == $quote) {
  158.             return substr($field, 1, -1);
  159.         }
  160.         return $field;
  161.     }
  162.  
  163.     /**
  164.     * Reads a row of data as an array from a CSV file. It's able to
  165.     * read memo fields with multiline data.
  166.     *
  167.     * @param string $file   The filename where to write the data
  168.     * @param array  &$conf   The configuration of the dest CSV
  169.     *
  170.     * @return mixed Array with the data read or false on error/no more data
  171.     */
  172.     function readQuoted($file, &$conf)
  173.     {
  174.         if (!$fp = File_CSV::getPointer($file, $conf, FILE_MODE_READ)) {
  175.             return false;
  176.         }
  177.         $buff = $c = null;
  178.         $ret  = array();
  179.         $i = 1;
  180.         $in_quote = false;
  181.         $quote = $conf['quote'];
  182.         $f = $conf['fields'];
  183.         while (($ch = fgetc($fp)) !== false) {
  184.             $prev = $c;
  185.             $c = $ch;
  186.             // Common case
  187.             if ($c != $quote && $c != $conf['sep'] && $c != "\n") {
  188.                 $buff .= $c;
  189.                 continue;
  190.             }
  191.             if ($c == $quote && $quote &&
  192.                 ($prev == $conf['sep'] || $prev == "\n" || $prev === null))
  193.             {
  194.                 $in_quote = true;
  195.             } elseif ($in_quote) {
  196.                 // When ends quote
  197.                 if ($c == $conf['sep'] && $prev == $conf['quote']) {
  198.                     $in_quote = false;
  199.                 } elseif ($c == "\n") {
  200.                     $sub = ($prev == "\r") ? 2 : 1;
  201.                     if ((strlen($buff) >= $sub) &&
  202.                         ($buff{strlen($buff) - $sub} == $quote))
  203.                     {
  204.                         $in_quote = false;
  205.                     }
  206.                 }
  207.             }
  208.             if (!$in_quote && ($c == $conf['sep'] || $c == "\n")) {
  209.                 // More fields than expected
  210.                 if (($c == $conf['sep']) && ((count($ret) + 1) == $f)) {
  211.                     while ($c != "\n") {
  212.                         $c = fgetc($fp);
  213.                     }
  214.                     File_CSV::raiseError("Read more fields than the ".
  215.                                          "expected ".$conf['fields']);
  216.                     return true;
  217.                 }
  218.                 // Less fields than expected
  219.                 if (($c == "\n") && ($i != $f)) {
  220.                     File_CSV::raiseError("Read wrong fields number count: '". $i .
  221.                                          "' expected ".$conf['fields']);
  222.                     return true;
  223.                 }
  224.                 if ($prev == "\r") {
  225.                     $buff = substr($buff, 0, -1);
  226.                 }
  227.                 $ret[] = File_CSV::unquote($buff, $quote);
  228.                 if (count($ret) == $f) {
  229.                     return $ret;
  230.                 }
  231.                 $buff = '';
  232.                 $i++;
  233.                 continue;
  234.             }
  235.             $buff .= $c;
  236.         }
  237.         return !feof($fp) ? $ret : false;
  238.     }
  239.  
  240.     /**
  241.     * Reads a "row" from a CSV file and return it as an array
  242.     *
  243.     * @param string $file The CSV file
  244.     * @param array  &$conf The configuration of the dest CSV
  245.     *
  246.     * @return mixed Array or false
  247.     */
  248.     function read($file, &$conf)
  249.     {
  250.         if (!$fp = File_CSV::getPointer($file, $conf, FILE_MODE_READ)) {
  251.             return false;
  252.         }
  253.         // The size is limited to 4K
  254.         if (!$line   = fgets($fp, 4096)) {
  255.             return false;
  256.         }
  257.         $fields = explode($conf['sep'], $line);
  258.         if ($conf['quote']) {
  259.             $last =& $fields[count($fields) - 1];
  260.             // Fallback to read the line with readQuoted when guess
  261.             // that the simple explode won't work right
  262.             if (($last{strlen($last) - 1} == "\n"
  263.                 && $last{0} == $conf['quote']
  264.                 && $last{strlen(rtrim($last)) - 1} != $conf['quote'])
  265.                 ||
  266.                 (count($fields) != $conf['fields'])
  267.                 // XXX perhaps there is a separator inside a quoted field
  268.                 //preg_match("|{$conf['quote']}.*{$conf['sep']}.*{$conf['quote']}|U", $line)
  269.                 )
  270.             {
  271.                 $len = strlen($line);
  272.                 fseek($fp, -1 * strlen($line), SEEK_CUR);
  273.                 return File_CSV::readQuoted($file, $conf);
  274.             } else {
  275.                 $last = rtrim($last);
  276.                 foreach ($fields as $k => $v) {
  277.                     $fields[$k] = File_CSV::unquote($v, $conf['quote']);
  278.                 }
  279.             }
  280.         }
  281.         if (count($fields) != $conf['fields']) {
  282.             File_CSV::raiseError("Read wrong fields number count: '". count($fields) .
  283.                                   "' expected ".$conf['fields']);
  284.             return true;
  285.         }
  286.         return $fields;
  287.     }
  288.  
  289.     /**
  290.     * Internal use only, will be removed in the future
  291.     *
  292.     * @param string $str The string to debug
  293.     * @access private
  294.     */
  295.     function _dbgBuff($str)
  296.     {
  297.         if (strpos($str, "\r") !== false) {
  298.             $str = str_replace("\r", "_r_", $str);
  299.         }
  300.         if (strpos($str, "\n") !== false) {
  301.             $str = str_replace("\n", "_n_", $str);
  302.         }
  303.         if (strpos($str, "\t") !== false) {
  304.             $str = str_replace("\t", "_t_", $str);
  305.         }
  306.         echo "buff: ($str)\n";
  307.     }
  308.  
  309.     /**
  310.     * Writes a struc (array) in a file as CSV
  311.     *
  312.     * @param string $file   The filename where to write the data
  313.     * @param array  $fields Ordered array with the data
  314.     * @param array  &$conf   The configuration of the dest CSV
  315.     *
  316.     * @return bool True on success false otherwise
  317.     */
  318.     function write($file, $fields, &$conf)
  319.     {
  320.         if (!$fp = File_CSV::getPointer($file, $conf, FILE_MODE_WRITE)) {
  321.             return false;
  322.         }
  323.         if (count($fields) != $conf['fields']) {
  324.             File_CSV::raiseError("Wrong fields number count: '". count($fields) .
  325.                                   "' expected ".$conf['fields']);
  326.             return true;
  327.         }
  328.         $write = '';
  329.         for ($i = 0; $i < count($fields); $i++) {
  330.             if (!is_numeric($fields[$i]) && $conf['quote']) {
  331.                 $write .= $conf['quote'] . $fields[$i] . $conf['quote'];
  332.             } else {
  333.                 $write .= $fields[$i];
  334.             }
  335.             if ($i < (count($fields) - 1)) {
  336.                 $write .= $conf['sep'];
  337.             } else {
  338.                 $write .= $conf['crlf'];
  339.             }
  340.         }
  341.         if (!fwrite($fp, $write)) {
  342.             return File_CSV::raiseError('Can not write to file');
  343.         }
  344.         return true;
  345.     }
  346.  
  347.     /**
  348.     * Discover the format of a CSV file (the number of fields, the separator
  349.     * and if it quote string fields)
  350.     *
  351.     * @param string the CSV file name
  352.     * @return mixed Assoc array or false
  353.     */
  354.     function discoverFormat($file)
  355.     {
  356.         if (!$fp = @fopen($file, 'r')) {
  357.             return File_CSV::raiseError("Could not open file: $file");
  358.         }
  359.         $seps = array("\t", ';', ':', ',');
  360.         $matches = array();
  361.         // Take the first 10 lines and store the number of ocurrences
  362.         // for each separator in each line
  363.         for ($i = 0; ($i < 10) && ($line = fgets($fp, 4096)); $i++) {
  364.             foreach ($seps as $sep) {
  365.                 $matches[$sep][$i] = substr_count($line, $sep);
  366.             }
  367.         }
  368.         $final = array();
  369.         // Group the results by amount of equal ocurrences
  370.         foreach ($matches as $sep => $res) {
  371.             $times = array();
  372.             $times[0] = 0;
  373.             foreach ($res as $k => $num) {
  374.                 if ($num > 0) {
  375.                     $times[$num] = (isset($times[$num])) ? $times[$num] + 1 : 1;
  376.                 }
  377.             }
  378.             arsort($times);
  379.             $fields[$sep] = key($times);
  380.             $amount[$sep] = $times[key($times)];
  381.         }
  382.         arsort($amount);
  383.         $sep    = key($amount);
  384.         $fields = $fields[$sep];
  385.         if (empty($fields)) {
  386.             return File_CSV::raiseError('Could not discover the separator');
  387.         }
  388.         $conf['fields'] = $fields + 1;
  389.         $conf['sep']    = $sep;
  390.         // Test if there are fields with quotes arround in the first 5 lines
  391.         $quotes = '"\'';
  392.         $quote  = null;
  393.         rewind($fp);
  394.         for ($i = 0; ($i < 5) && ($line = fgets($fp, 4096)); $i++) {
  395.             if (preg_match("|$sep([$quotes]).*([$quotes])$sep|U", $line, $match)) {
  396.                 if ($match[1] == $match[2]) {
  397.                     $quote = $match[1];
  398.                     break;
  399.                 }
  400.             }
  401.             if (preg_match("|^([$quotes]).*([$quotes])$sep|", $line, $match)
  402.                 || preg_match("|([$quotes]).*([$quotes])$sep\s$|Us", $line, $match))
  403.             {
  404.                 if ($match[1] == $match[2]) {
  405.                     $quote = $match[1];
  406.                     break;
  407.                 }
  408.             }
  409.         }
  410.         $conf['quote'] = $quote;
  411.         fclose($fp);
  412.         // XXX What about trying to discover the "header"?
  413.         return $conf;
  414.     }
  415. }
  416. ?>